/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.csp.sentinel.slots.block.flow.controller;

import com.alibaba.csp.sentinel.node.Node;
import com.alibaba.csp.sentinel.node.OccupyTimeoutProperty;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.PriorityWaitException;
import com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController;
import com.alibaba.csp.sentinel.util.TimeUtil;

/**
 * Default throttling controller (immediately reject strategy).
 *
 * @author jialiang.linjl
 * @author Eric Zhao
 */
public class DefaultController implements TrafficShapingController {

    private static final int DEFAULT_AVG_USED_TOKENS = 0;

    private double count;
    private int grade;

    public DefaultController(double count, int grade) {
        this.count = count;
        this.grade = grade;
    }

    @Override
    public boolean canPass(Node node, int acquireCount) {
        return canPass(node, acquireCount, false);
    }

    @Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        int curCount = avgUsedTokens(node);
        if (curCount + acquireCount > count) {
            /**
             * hujianfeng
             * 如果被限流了，但是又是prioritized模式，则会继续往下走.
             *
             * 网上的说明：
             * Sentinel 1.5.0 对底层的滑动窗口统计结构进行了升级，添加了“占用”机制，允许在当前 QPS 已经达到限流阈值时，同个资源高优先级的请求提前占用未来时间窗口的配额数，
             * 等待到对应时间窗口到达时直接通过，从而可以实现“最终通过”的效果而不是被立即拒绝；而同个资源低优先级的请求则不能占用未来的配额，阈值达到时就会被限流。
             * Sentinel 1.5.0 引入了 FutureBucketLeapArray，这是一种特殊的滑动窗口，仅维持当前时间以后的格子，从而可以用于统计未来被预先占用的配额数目。Sentinel 将
             * 普通的滑动窗口与 FutureBucketLeapArray 组合成可占用的滑动窗口OccupiableBucketLeapArray，从而实现了“部分高优先级请求最终通过”的效果。我们可以调用
             * SphU.entryWithPriority(resourceName) 来标识本次调用为高优先级（prioritized = true）。
             */
            if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
                long currentTime;
                long waitInMs;
                currentTime = TimeUtil.currentTimeMillis();
                waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
                if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                    /**
                     * hujianfeng
                     * 这里重点分析：
                     * addWaitingRequest方法的第一个入参是"currentTime + waitInMs"，很明显，这是一个"未来"的时间，是当前时间在未来waitInMs毫秒之后的时间点。
                     * 这个waitInMs是怎么算出来的，先不关心，主要关心后面的操作。
                     * 1、首先，因为当前的时间窗口qps已经满载了，所以，会新开（或使用已经新开过的）一个未来的时间窗口（对应的时间点就是"currentTime + waitInMs"），在窗口的统计信息里中计入当前的调用。
                     * 2、然后，让线程sleep waitInMs毫秒，正好等到这个未来的时间点（"currentTime + waitInMs"），然后放行线程。
                     * 3、放行线程之后，就会在后续Slot链的StatisticSlot中(对PriorityWaitException异常的catch代码块中），对当前的调用做其他（除QPS之外的）统计。
                     * 标记[5-1]
                     */
                    node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                    node.addOccupiedPass(acquireCount);
                    sleep(waitInMs);

                    // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                    throw new PriorityWaitException(waitInMs);
                }
            }
            return false;
        }
        return true;
    }

    private int avgUsedTokens(Node node) {
        if (node == null) {
            return DEFAULT_AVG_USED_TOKENS;
        }
        return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
    }

    private void sleep(long timeMillis) {
        try {
            Thread.sleep(timeMillis);
        } catch (InterruptedException e) {
            // Ignore.
        }
    }
}
